Completed
Push — master ( c8ba67...05deb1 )
by Alejandro
21s queued 12s
created

ShortUrlVisits.js ➔ render   B

Complexity

Conditions 5

Size

Total Lines 95
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 80
dl 0
loc 95
ccs 16
cts 16
cp 1
rs 7.1878
c 0
b 0
f 0
cc 5
crap 5

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
import { isEmpty, values } from 'ramda';
2
import React, { useState, useEffect } from 'react';
3
import { Button, Card, Collapse } from 'reactstrap';
4
import PropTypes from 'prop-types';
5
import qs from 'qs';
6
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7
import { faChevronDown as chevronDown } from '@fortawesome/free-solid-svg-icons';
8
import DateRangeRow from '../utils/DateRangeRow';
9
import Message from '../utils/Message';
10
import { formatDate } from '../utils/helpers/date';
11
import { useToggle } from '../utils/helpers/hooks';
12
import SortableBarGraph from './SortableBarGraph';
13
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
14
import VisitsHeader from './VisitsHeader';
15
import GraphCard from './GraphCard';
16
import { shortUrlDetailType } from './reducers/shortUrlDetail';
17
import VisitsTable from './VisitsTable';
18
19 1
const propTypes = {
20
  match: PropTypes.shape({
21
    params: PropTypes.object,
22
  }),
23
  location: PropTypes.shape({
24
    search: PropTypes.string,
25
  }),
26
  getShortUrlVisits: PropTypes.func,
27
  shortUrlVisits: shortUrlVisitsType,
28
  getShortUrlDetail: PropTypes.func,
29
  shortUrlDetail: shortUrlDetailType,
30
  cancelGetShortUrlVisits: PropTypes.func,
31
  matchMedia: PropTypes.func,
32
};
33
34 18
const highlightedVisitsToStats = (highlightedVisits, prop) => highlightedVisits.reduce((acc, highlightedVisit) => {
35 2
  if (!acc[highlightedVisit[prop]]) {
36
    acc[highlightedVisit[prop]] = 0;
37
  }
38
39
  acc[highlightedVisit[prop]] += 1;
40
41
  return acc;
42
}, {});
43 1
const format = formatDate();
44
let memoizationId;
45
let timeWhenMounted;
46
47 1
const ShortUrlVisits = ({ processStatsFromVisits }, OpenMapModalBtn) => {
48 7
  const ShortUrlVisitsComp = ({
49
    match,
50
    location,
51
    shortUrlVisits,
52
    shortUrlDetail,
53
    getShortUrlVisits,
54
    getShortUrlDetail,
55
    cancelGetShortUrlVisits,
56
    matchMedia = window.matchMedia,
57
  }) => {
58 10
    const [ startDate, setStartDate ] = useState(undefined);
59 10
    const [ endDate, setEndDate ] = useState(undefined);
60 10
    const [ showTable, toggleTable ] = useToggle();
61 10
    const [ tableIsSticky, , setSticky, unsetSticky ] = useToggle();
62 10
    const [ highlightedVisits, setHighlightedVisits ] = useState([]);
63 10
    const [ isMobileDevice, setIsMobileDevice ] = useState(false);
64 10
    const determineIsMobileDevice = () => setIsMobileDevice(matchMedia('(max-width: 991px)').matches);
65
66 10
    const { params } = match;
67 10
    const { shortCode } = params;
68 10
    const { search } = location;
69 10
    const { domain } = qs.parse(search, { ignoreQueryPrefix: true });
70
71 10
    const loadVisits = () => {
72
      const start = format(startDate);
73
      const end = format(endDate);
74
75
      // While the "page" is loaded, use the timestamp + filtering dates as memoization IDs for stats calculations
76
      memoizationId = `${timeWhenMounted}_${shortCode}_${start}_${end}`;
77
      getShortUrlVisits(shortCode, { startDate: start, endDate: end, domain });
78
    };
79
80 10
    useEffect(() => {
81
      timeWhenMounted = new Date().getTime();
82
      getShortUrlDetail(shortCode, domain);
83
      determineIsMobileDevice();
84
      window.addEventListener('resize', determineIsMobileDevice);
85
86
      return () => {
87
        cancelGetShortUrlVisits();
88
        window.removeEventListener('resize', determineIsMobileDevice);
89
      };
90
    }, []);
91 10
    useEffect(() => {
92
      loadVisits();
93
    }, [ startDate, endDate ]);
94
95 10
    const { visits, loading, loadingLarge, error } = shortUrlVisits;
96 10
    const showTableControls = !loading && visits.length > 0;
97
98 10
    const renderVisitsContent = () => {
99 10
      if (loading) {
100 2
        const message = loadingLarge ? 'This is going to take a while... :S' : 'Loading...';
101
102 2
        return <Message loading>{message}</Message>;
103
      }
104
105 8
      if (error) {
106 1
        return (
107
          <Card className="mt-4" body inverse color="danger">
108
            An error occurred while loading visits :(
109
          </Card>
110
        );
111
      }
112
113 7
      if (isEmpty(visits)) {
114 1
        return <Message>There are no visits matching current filter  :(</Message>;
115
      }
116
117 6
      const { os, browsers, referrers, countries, cities, citiesForMap } = processStatsFromVisits(
118
        { id: memoizationId, visits }
119
      );
120 6
      const mapLocations = values(citiesForMap);
121
122 6
      return (
123
        <div className="row">
124
          <div className="col-xl-4 col-lg-6">
125
            <GraphCard title="Operating systems" stats={os} />
126
          </div>
127
          <div className="col-xl-4 col-lg-6">
128
            <GraphCard title="Browsers" stats={browsers} />
129
          </div>
130
          <div className="col-xl-4">
131
            <SortableBarGraph
132
              title="Referrers"
133
              stats={referrers}
134
              withPagination={false}
135
              highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
136
              sortingItems={{
137
                name: 'Referrer name',
138
                amount: 'Visits amount',
139
              }}
140
            />
141
          </div>
142
          <div className="col-lg-6">
143
            <SortableBarGraph
144
              title="Countries"
145
              stats={countries}
146
              highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')}
147
              sortingItems={{
148
                name: 'Country name',
149
                amount: 'Visits amount',
150
              }}
151
            />
152
          </div>
153
          <div className="col-lg-6">
154
            <SortableBarGraph
155
              title="Cities"
156
              stats={cities}
157
              highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
158
              extraHeaderContent={(activeCities) =>
159 2
                mapLocations.length > 0 &&
160
                <OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
161
              }
162
              sortingItems={{
163
                name: 'City name',
164
                amount: 'Visits amount',
165
              }}
166
            />
167
          </div>
168
        </div>
169
      );
170
    };
171
172 10
    return (
173
      <React.Fragment>
174
        <VisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} />
175
176
        <section className="mt-4">
177
          <div className="row flex-md-row-reverse">
178
            <div className="col-lg-8 col-xl-6">
179
              <DateRangeRow
180
                startDate={startDate}
181
                endDate={endDate}
182
                onStartDateChange={setStartDate}
183
                onEndDateChange={setEndDate}
184
              />
185
            </div>
186
            <div className="col-lg-4 col-xl-6 mt-4 mt-lg-0">
187
              {showTableControls && (
188
                <Button
189
                  outline
190
                  block={isMobileDevice}
191
                  onClick={toggleTable}
192
                >
193
                  {showTable ? 'Hide' : 'Show'} table{' '}
194
                  <FontAwesomeIcon icon={chevronDown} rotation={showTable ? 180 : undefined} />
195
                </Button>
196
              )}
197
            </div>
198
          </div>
199
        </section>
200
201
        {showTableControls && (
202
          <Collapse
203
            isOpen={showTable}
204
205
            // Enable stickiness only when there's no CSS animation, to avoid weird rendering effects
206
            onEntered={setSticky}
207
            onExiting={unsetSticky}
208
          >
209
            <VisitsTable visits={visits} isSticky={tableIsSticky} onVisitsSelected={setHighlightedVisits} />
210
          </Collapse>
211
        )}
212
213
        <section>
214
          {renderVisitsContent()}
215
        </section>
216
      </React.Fragment>
217
    );
218
  };
219
220 7
  ShortUrlVisitsComp.propTypes = propTypes;
221
222 7
  return ShortUrlVisitsComp;
223
};
224
225
export default ShortUrlVisits;
226